
// ===============================================================================================================
// -*- C++ -*-
//
// Program.cpp - Program entry point.
//
// Copyright (c) 2010 Guilherme R. Lampert
// guilherme.ronaldo.lampert@gmail.com
//
// This code is licenced under the MIT license.
//
// This software is provided "as is" without express or implied
// warranties. You may freely copy and compile this source into
// applications you distribute provided that the copyright text
// above is included in the resulting source code.
//
// ===============================================================================================================

#include "Program.hpp"
#include <time.h>

#if defined (_MSC_VER) && (_MSC_VER > 1000) && !defined (_DEBUG)
// Suppress the console window for Windows release builds:
#pragma comment (linker, "/subsystem:\"windows\" /entry:\"mainCRTStartup\"")
#endif

// == Class Program ==

static const int CMD_OPEN_FILE = 26;
static const int CMD_QUIT      = 27;

// ======================================================
// Static Data:

int Program::m_MenuIds[2];
Program::UserObjectInfo Program::m_LoadedObj;
std::vector<std::string> Program::m_vMeshGroups;

int Program::m_WinId(-1);
int Program::m_window_width(0);
int Program::m_window_height(0);

Vec2 Program::m_LastMousePos;
Vec2 Program::m_CurrentMousePos;
bool Program::m_MouseMov(false);

float Program::m_AngleX(0.0f);
float Program::m_AngleY(0.0f);
float Program::m_Zoom(-20.0f);

bool Program::m_Wireframe(false);
bool Program::m_ShowText(true);

// ======================================================

void Program::Initialize(int window_width, int window_height, int argc, char * argv[])
{
	// Init Glut:
	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH);
	glutInitWindowSize(window_width, window_height);
	glutInitWindowPosition(0,0);
	m_WinId = glutCreateWindow("Wavefront Object Viewer");

	// Set Glut function pointers:
	glutReshapeFunc(Program::ResizeWindow);
	glutDisplayFunc(Program::Render);
	glutIdleFunc(Program::Render);
	glutKeyboardFunc(Program::KeyboardCallback);
	glutMouseFunc(Program::MouseCallback);
	glutMotionFunc(Program::MouseMotionCallback);
	atexit(Program::Shutdown);

	ResizeWindow(window_width, window_height);

	glCullFace(GL_NONE);
	glShadeModel(GL_SMOOTH);
	glEnable(GL_DEPTH_TEST);

	memset(&m_LoadedObj, 0, sizeof(Program::UserObjectInfo));
	m_LoadedObj.nSelectedGroup = -1; // Set to an invalid value on startup.

	m_MenuIds[0] = -1; // Set to an invalid menu Id.
	m_MenuIds[1] = -1;

	SetUpMenus(NULL);

	glEnable(GL_LIGHT0);
	glEnable(GL_LIGHTING);
	glClearColor(0.3f, 0.3f, 0.3f, 1.0f);
}

void Program::Execute()
{
	glutMainLoop();
}

void Program::Shutdown()
{
	if (m_MenuIds[0] != -1)
		glutDestroyMenu(m_MenuIds[0]);

	if (m_MenuIds[1] != -1)
		glutDestroyMenu(m_MenuIds[1]);

	glutDestroyWindow(m_WinId);

	m_vMeshGroups.clear();

	if (m_LoadedObj.pMesh != NULL)
	{
		delete m_LoadedObj.pMesh;
		m_LoadedObj.pMesh = NULL;
	}

	Texture2DManager::Kill();
}

void Program::Render()
{
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glLoadIdentity();
	glInitNames();

	if (m_ShowText)
	{
		glBegin2D(m_window_width, m_window_height);

		// Display mesh status and FPS:
		glColor3f(1.0f, 1.0f, 1.0f);

		if (m_LoadedObj.pMesh != NULL)
		{
			glPrintf(10, m_window_height-20, "Object loaded in %.3f seconds", m_LoadedObj.fLoadTimeInSecs);
			glPrintf(10, m_window_height-40, "Vertices: %u", m_LoadedObj.nVertexCount);
			glPrintf(10, m_window_height-60, "Normals:  %u", m_LoadedObj.nNormalCount);
			glPrintf(10, m_window_height-80, "UVs:      %u", m_LoadedObj.nUVCount);
		}

		glPrintf(10, 20, "FPS: %i", CalcFPS());

		// Little help display:
		glPrintf(m_window_width - 160, 140, "        + Zoom [Z]");
		glPrintf(m_window_width - 160, 120, "        - Zoom [X]");
		glPrintf(m_window_width - 160, 100, "       Verbose [V]");
		glPrintf(m_window_width - 160, 80,  "     Wireframe [W]");
		glPrintf(m_window_width - 160, 60,  "  Flat Shading [F]");
		glPrintf(m_window_width - 160, 40,  "Smooth Shading [S]");
		glPrintf(m_window_width - 200, 20,  "[ALT] + Mouse to rotate");

		glEnd2D();
	}

	if (m_LoadedObj.pMesh != NULL)
	{
		glPushMatrix();

		glTranslatef(0.0f, 0.0f, m_Zoom);
		glRotatef(-(m_AngleY), 1.0f, 0.0f, 0.0f);
		glRotatef(-(m_AngleX), 0.0f, 1.0f, 0.0f);

		Mesh::RenderMode renderMode;

		if (m_Wireframe)
		{
			renderMode = Mesh::Wireframe;
			glLineWidth(1.0f);

			// Wireframe line color:
			glColor3f(0.6f, 0.6f, 0.77f);
		}
		else
		{
			renderMode = Mesh::Solid;
			glLineWidth(2.0f);
		}

		// Render everything:
		if (m_LoadedObj.nDrawByGroupID == -1)
		{
			// First pass, default rendering:
			if ((m_LoadedObj.nNormalCount > 0) && (!m_Wireframe))
			{
				glEnable(GL_LIGHTING);
			}

			m_LoadedObj.pMesh->SetRenderMode(renderMode);
			Mesh::GroupMap::const_iterator GroupIndex = m_LoadedObj.pMesh->Groups().begin();
			Mesh::GroupMap::const_iterator LastGroup  = m_LoadedObj.pMesh->Groups().end();

			GLuint name = 0;

			while (GroupIndex != LastGroup)
			{
				glPushName(name);
				m_LoadedObj.pMesh->Render(GroupIndex);
				glPopName();

				++name;
				++GroupIndex;
			}

			if ((m_LoadedObj.nNormalCount > 0) && (!m_Wireframe))
			{
				glDisable(GL_LIGHTING);
			}

			// Second pass, draw group outline:
			if (m_LoadedObj.nSelectedGroup != -1)
			{
				glColor3f(0.2f, 0.8f, 0.35f); // Give it a cool Maya-like outline color :)
				m_LoadedObj.pMesh->SetRenderMode(Mesh::Wireframe);
				m_LoadedObj.pMesh->Render(m_LoadedObj.pMesh->Groups().find(m_vMeshGroups[m_LoadedObj.nSelectedGroup]));
			}
		}
		else // Render the selected group only:
		{
			if ((m_LoadedObj.nNormalCount > 0) && (!m_Wireframe))
			{
				glEnable(GL_LIGHTING);
			}

			m_LoadedObj.pMesh->SetRenderMode(renderMode);
			m_LoadedObj.pMesh->Render(m_LoadedObj.pMesh->Groups().find(m_vMeshGroups[m_LoadedObj.nDrawByGroupID]));

			if ((m_LoadedObj.nNormalCount > 0) && (!m_Wireframe))
			{
				glDisable(GL_LIGHTING);
			}
		}

		glPopMatrix();
	}

	glFlush();
	glutSwapBuffers();
}

void Program::ResizeWindow(int window_width, int window_height)
{
	m_window_width = window_width;
	m_window_height = window_height;

	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glViewport(0, 0, m_window_width, m_window_height);

	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();

	gluPerspective(45.0f, (static_cast<double>(m_window_width) / static_cast<double>(m_window_height)), 0.5, 10000.0);

	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();

	glutPostRedisplay();
}

void Program::KeyboardCallback(unsigned char key, int x, int y)
{
	switch (key)
	{
	case 'z': case 'Z':
		{
			m_Zoom += 1.5f;
			break;
		}
	case 'x': case 'X':
		{
			m_Zoom -= 1.5f;
			break;
		}
	case 'v': case 'V':
		{
			m_ShowText = !m_ShowText;
			break;
		}
	case 'w': case 'W':
		{
			m_Wireframe = !m_Wireframe;
			break;
		}
	case 'f': case 'F':
		{
			glShadeModel(GL_FLAT);
			break;
		}
	case 's': case 'S':
		{
			glShadeModel(GL_SMOOTH);
			break;
		}
	case CMD_QUIT: // ESCAPE key pressed
		{
			exit(0);
			break;
		}
	default:
		break;
	} // End of switch (key)
}

void Program::MouseCallback(int button, int state, int x, int y)
{
	int modifiers;

	switch (button)
	{
	case GLUT_LEFT_BUTTON:
		{
			switch (state)
			{
			case GLUT_DOWN:
				{
					modifiers = glutGetModifiers();
					if (modifiers & GLUT_ACTIVE_ALT)
					{
						m_LastMousePos.x = m_CurrentMousePos.x = x;
						m_LastMousePos.y = m_CurrentMousePos.y = y;
						m_MouseMov = true;
					}
					else // Check for object selection:
					{
						if (m_LoadedObj.pMesh != 0) // Only if we have a mesh on the screen
						{
							m_LoadedObj.nSelectedGroup = GetClickedGroup(x, y);
						}
					}
					break;
				}
			case GLUT_UP:
				{
					m_MouseMov = false;
					break;
				}
			default:
				break;
			} // End of switch (state)
			break;
		}
	default:
		break;
	} // End of switch (button)
}

void Program::MouseMotionCallback(int x, int y)
{
	m_CurrentMousePos.x = x;
	m_CurrentMousePos.y = y;

	if (m_MouseMov)
	{
		m_AngleX -= (m_CurrentMousePos.x - m_LastMousePos.x);
		m_AngleY -= (m_CurrentMousePos.y - m_LastMousePos.y);
	}

	m_LastMousePos.x = m_CurrentMousePos.x;
	m_LastMousePos.y = m_CurrentMousePos.y;
}

void Program::MenuCallback(int value)
{
	const char * file_path;

	switch (value)
	{
	case CMD_OPEN_FILE:
		{
			// Open a file
			file_path = OpenFileDialog(0, OPEN_FILE, "Wavefront Object Files (*.obj)", "*.obj");
			if (!file_path)
			{
				break;
			}
			else
			{
				LoadObject(file_path);
			}
			break;
		}
	case CMD_QUIT:
		{
			// Quitter
			exit(0);
			break;
		}
	default:
		{
			m_LoadedObj.nDrawByGroupID = value;
			break;
		}
	} // End of switch (value)

	glutPostRedisplay();
}

void Program::LoadObject(const std::string & filename)
{
	clock_t end_time, start_time;

	if (m_LoadedObj.pMesh != NULL)
	{
		delete m_LoadedObj.pMesh;
		m_LoadedObj.pMesh = NULL;
	}

	try {

		start_time = clock();

		m_LoadedObj.pMesh = new WavefrontObject(filename);

		end_time = clock();

		m_LoadedObj.fLoadTimeInSecs = (float)(end_time - start_time)/CLOCKS_PER_SEC;

		m_LoadedObj.nNormalCount = m_LoadedObj.pMesh->NormalCount();
		m_LoadedObj.nVertexCount = m_LoadedObj.pMesh->VertexCount();
		m_LoadedObj.nUVCount = m_LoadedObj.pMesh->TexCoordCount();

		m_LoadedObj.nDrawByGroupID = -1; // -1 = Render the entire mesh.
		m_LoadedObj.nSelectedGroup = -1; // -1 = No groups selected.
		m_vMeshGroups.clear();

		SetUpMenus(m_LoadedObj.pMesh);

		std::cout << "OBJ File \"" << filename << "\" loaded successfully!" << std::endl << std::endl;
	}
	catch (std::runtime_error & except) {

		LOG_ERROR(except.what());
		delete m_LoadedObj.pMesh;
		m_LoadedObj.pMesh = NULL;
	}
}

void Program::SetUpMenus(const Mesh * pUserMesh)
{
	if (m_MenuIds[0] != -1)
		glutDestroyMenu(m_MenuIds[0]);

	if (m_MenuIds[1] != -1)
		glutDestroyMenu(m_MenuIds[1]);

	m_MenuIds[0] = glutCreateMenu(Program::MenuCallback);
	m_MenuIds[1] = glutCreateMenu(Program::MenuCallback);

	glutSetMenu(m_MenuIds[0]);
	glutAddSubMenu("Render By Group", m_MenuIds[1]);
	glutAddMenuEntry("Open File", CMD_OPEN_FILE);
	glutAddMenuEntry("Exit", CMD_QUIT);

	if (pUserMesh != NULL)
	{
		glutSetMenu(m_MenuIds[1]);
		glutAddMenuEntry("All Groups", -1);

		Mesh::GroupMap::const_iterator GroupIndex = pUserMesh->Groups().begin();
		Mesh::GroupMap::const_iterator LastGroup  = pUserMesh->Groups().end();

		int id = 0;

		while (GroupIndex != LastGroup)
		{
			glutAddMenuEntry((*GroupIndex).first.c_str(), id);

			m_vMeshGroups.push_back((*GroupIndex).first);

			++id;
			++GroupIndex;
		}
	}

	glutSetMenu(m_MenuIds[0]);
	glutAttachMenu(GLUT_RIGHT_BUTTON);
}

int Program::GetClickedGroup(int x, int y)
{
	// Do mesh picking:

	int objectsFound = 0;
	int	viewportCoords[4];

	// "For every object there is 4 values. The first value is the number of names
	// in the name stack at the time of the event, followed  by the minimum and maximum
	// depth values of all vertices that hit since the previous event, then followed by
	// the name stack contents, bottom name first." - MSDN

	std::vector<GLuint> selectionBuffer(m_vMeshGroups.size() * 4); // Total OBJ Groups * 4

	glSelectBuffer(selectionBuffer.size(), &selectionBuffer[0]);

	glGetIntegerv(GL_VIEWPORT, viewportCoords);

	glMatrixMode(GL_PROJECTION);

	glPushMatrix();

	glRenderMode(GL_SELECT);

	glLoadIdentity();

	gluPickMatrix(x, viewportCoords[3] - y, 2, 2, viewportCoords);

	gluPerspective(45.0f, (static_cast<float>(viewportCoords[2]) / static_cast<float>(viewportCoords[3])), 0.1f, 150.0f);

	glMatrixMode(GL_MODELVIEW);

	// Disable the text if enabled to avoid interference
	bool prevState = m_ShowText;
	m_ShowText = false;

	Render(); // Render to selection buffer...

	// Reset text to default
	m_ShowText = prevState;

	objectsFound = glRenderMode(GL_RENDER);

	glMatrixMode(GL_PROJECTION);

	glPopMatrix();

	glMatrixMode(GL_MODELVIEW);

	if (objectsFound > 0) // Find the actual object that was clicked:
	{
		unsigned int lowestDepth = selectionBuffer[1];

		int selectedObject = selectionBuffer[3];

		for (int i = 1; i < objectsFound; i++)
		{
			if (selectionBuffer[(i * 4) + 1] < lowestDepth)
			{
				lowestDepth = selectionBuffer[(i * 4) + 1];

				selectedObject = selectionBuffer[(i * 4) + 3];
			}
		}

		return (selectedObject);
	}

	return (-1); // -1 = No valid objects clicked.
}

// main() - Application Entry Point:
int main(int argc, char ** argv)
{
	Program::Initialize(800, 600, argc, argv);

	Program::Execute();

	return (0);
}
